package org.erikaredmark.monkeyshines.editor.dialog; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.SpringLayout; import javax.swing.JLabel; import javax.swing.JTextField; import java.awt.Rectangle; import javax.swing.JSpinner; import javax.swing.JButton; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.SpinnerNumberModel; import org.erikaredmark.monkeyshines.AnimationSpeed; import org.erikaredmark.monkeyshines.AnimationType; import org.erikaredmark.monkeyshines.ImmutablePoint2D; import org.erikaredmark.monkeyshines.ImmutableRectangle; import org.erikaredmark.monkeyshines.Sprite; import org.erikaredmark.monkeyshines.Sprite.ForcedDirection; import org.erikaredmark.monkeyshines.Sprite.SpriteType; import org.erikaredmark.monkeyshines.Sprite.TwoWayFacing; import org.erikaredmark.monkeyshines.resource.WorldResource; public class SpritePropertiesDialog extends JDialog { private static final long serialVersionUID = 1L; private JTextField txtTopLeftX; private JTextField txtTopLeftY; private JLabel lblWidth; private JLabel lblHeight; private JTextField txtWidth; private JTextField txtHeight; private JLabel lblVelocityx; private JLabel lblVelocityy; private JTextField txtVelocityX; private JTextField txtVelocityY; private JLabel lblStartX; private JLabel lblStartY; private JTextField txtStartX; private JTextField txtStartY; private JSpinner spriteIdSpinner; private JCheckBox forceDirection; private JComboBox<String> direction; private JComboBox<String> twoWayFacing; private final SpritePropertiesModel model; // True if user hits okay, false if otherwise private boolean okay = false; private SpritePropertiesDialog(WorldResource rsrc, SpritePropertiesModel initialModel) { model = initialModel; // Some action listeners refer to visual components that haven't been initialised when reading top-to-bottom. // These listeners won't ever fire until the object is fully constructed, so all listeners can assume instance // data of the object will be non-null when fired. SpringLayout springLayout = new SpringLayout(); getContentPane().setLayout(springLayout); JLabel lblTopleftx = new JLabel("Top-Left (X)"); springLayout.putConstraint(SpringLayout.NORTH, lblTopleftx, 10, SpringLayout.NORTH, this); springLayout.putConstraint(SpringLayout.WEST, lblTopleftx, 10, SpringLayout.WEST, this); getContentPane().add(lblTopleftx); JLabel lblToplefty = new JLabel("Top-Left (Y)"); springLayout.putConstraint(SpringLayout.WEST, lblToplefty, 0, SpringLayout.WEST, lblTopleftx); getContentPane().add(lblToplefty); txtTopLeftX = new JTextField(); springLayout.putConstraint(SpringLayout.NORTH, txtTopLeftX, -3, SpringLayout.NORTH, lblTopleftx); springLayout.putConstraint(SpringLayout.WEST, txtTopLeftX, 6, SpringLayout.EAST, lblTopleftx); getContentPane().add(txtTopLeftX); txtTopLeftX.setColumns(10); txtTopLeftX.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteBoundingBoxTopLeftX(Integer.parseInt(txtTopLeftX.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); txtTopLeftY = new JTextField(); springLayout.putConstraint(SpringLayout.WEST, txtTopLeftY, 6, SpringLayout.EAST, lblToplefty); springLayout.putConstraint(SpringLayout.NORTH, lblToplefty, 3, SpringLayout.NORTH, txtTopLeftY); springLayout.putConstraint(SpringLayout.NORTH, txtTopLeftY, 10, SpringLayout.SOUTH, txtTopLeftX); getContentPane().add(txtTopLeftY); txtTopLeftY.setColumns(10); txtTopLeftY.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteBoundingBoxTopLeftY(Integer.parseInt(txtTopLeftY.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); lblWidth = new JLabel("Width"); springLayout.putConstraint(SpringLayout.NORTH, lblWidth, 0, SpringLayout.NORTH, lblTopleftx); springLayout.putConstraint(SpringLayout.WEST, lblWidth, 49, SpringLayout.EAST, txtTopLeftX); getContentPane().add(lblWidth); lblHeight = new JLabel("Height"); springLayout.putConstraint(SpringLayout.NORTH, lblHeight, 16, SpringLayout.SOUTH, lblWidth); springLayout.putConstraint(SpringLayout.EAST, lblHeight, 0, SpringLayout.EAST, lblWidth); getContentPane().add(lblHeight); txtWidth = new JTextField(); springLayout.putConstraint(SpringLayout.NORTH, txtWidth, -3, SpringLayout.NORTH, lblTopleftx); springLayout.putConstraint(SpringLayout.WEST, txtWidth, 18, SpringLayout.EAST, lblWidth); getContentPane().add(txtWidth); txtWidth.setColumns(10); txtWidth.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteBoundingBoxWidth(Integer.parseInt(txtWidth.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); txtHeight = new JTextField(); springLayout.putConstraint(SpringLayout.NORTH, txtHeight, 10, SpringLayout.SOUTH, txtWidth); springLayout.putConstraint(SpringLayout.EAST, txtHeight, 0, SpringLayout.EAST, txtWidth); getContentPane().add(txtHeight); txtHeight.setColumns(10); txtHeight.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteBoundingBoxHeight(Integer.parseInt(txtHeight.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); lblVelocityx = new JLabel("Velocity (X)"); springLayout.putConstraint(SpringLayout.NORTH, lblVelocityx, 45, SpringLayout.SOUTH, lblToplefty); springLayout.putConstraint(SpringLayout.WEST, lblVelocityx, 0, SpringLayout.WEST, lblTopleftx); getContentPane().add(lblVelocityx); lblVelocityy = new JLabel("Velocity (Y)"); springLayout.putConstraint(SpringLayout.NORTH, lblVelocityy, 16, SpringLayout.SOUTH, lblVelocityx); springLayout.putConstraint(SpringLayout.WEST, lblVelocityy, 0, SpringLayout.WEST, lblTopleftx); getContentPane().add(lblVelocityy); txtVelocityX = new JTextField(); springLayout.putConstraint(SpringLayout.NORTH, txtVelocityX, -3, SpringLayout.NORTH, lblVelocityx); springLayout.putConstraint(SpringLayout.EAST, txtVelocityX, 0, SpringLayout.EAST, txtTopLeftX); getContentPane().add(txtVelocityX); txtVelocityX.setColumns(10); txtVelocityX.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteVelocityX(Integer.parseInt(txtVelocityX.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); txtVelocityY = new JTextField(); springLayout.putConstraint(SpringLayout.NORTH, txtVelocityY, -3, SpringLayout.NORTH, lblVelocityy); springLayout.putConstraint(SpringLayout.EAST, txtVelocityY, 0, SpringLayout.EAST, txtTopLeftX); getContentPane().add(txtVelocityY); txtVelocityY.setColumns(10); txtVelocityY.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteVelocityY(Integer.parseInt(txtVelocityY.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); lblStartX = new JLabel("Start (X)"); springLayout.putConstraint(SpringLayout.NORTH, lblStartX, 0, SpringLayout.NORTH, lblVelocityx); springLayout.putConstraint(SpringLayout.EAST, lblStartX, 0, SpringLayout.EAST, lblWidth); getContentPane().add(lblStartX); lblStartY = new JLabel("Start (Y)"); springLayout.putConstraint(SpringLayout.NORTH, lblStartY, 0, SpringLayout.NORTH, lblVelocityy); springLayout.putConstraint(SpringLayout.EAST, lblStartY, 0, SpringLayout.EAST, lblWidth); getContentPane().add(lblStartY); txtStartX = new JTextField(String.valueOf(model.getSpriteStartingLocation().x() ) ); springLayout.putConstraint(SpringLayout.NORTH, txtStartX, 0, SpringLayout.NORTH, txtVelocityX); springLayout.putConstraint(SpringLayout.EAST, txtStartX, 0, SpringLayout.EAST, txtWidth); getContentPane().add(txtStartX); txtStartX.setColumns(10); txtStartX.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteLocationX(Integer.parseInt(txtStartX.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); txtStartY = new JTextField(String.valueOf(model.getSpriteStartingLocation().y() ) ); springLayout.putConstraint(SpringLayout.NORTH, txtStartY, 0, SpringLayout.NORTH, txtVelocityY); springLayout.putConstraint(SpringLayout.WEST, txtStartY, 0, SpringLayout.WEST, txtWidth); getContentPane().add(txtStartY); txtStartY.setColumns(10); txtStartY.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { model.setSpriteLocationY(Integer.parseInt(txtStartY.getText() ) ); } @Override public void focusGained(FocusEvent e) { } }); final JComboBox<SpriteType> spriteType = new JComboBox<>(SpriteType.values() ); spriteType.setSelectedItem(model.getSpriteType() ); spriteType.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { final SpriteType type = (SpriteType)spriteType.getSelectedItem(); model.setSpriteType(type); } }); springLayout.putConstraint(SpringLayout.NORTH, spriteType, 5, SpringLayout.SOUTH, txtStartY); springLayout.putConstraint(SpringLayout.WEST, spriteType, 0, SpringLayout.WEST, lblTopleftx); getContentPane().add(spriteType); final SpriteAnimationCanvas spriteDrawCanvas = new SpriteAnimationCanvas(0, model.getAnimationType(), model.getAnimationSpeed(), rsrc); springLayout.putConstraint(SpringLayout.WEST, spriteDrawCanvas, 0, SpringLayout.WEST, txtVelocityY); spriteDrawCanvas.setBounds(new Rectangle(0, 0, 40, 40)); springLayout.putConstraint(SpringLayout.NORTH, spriteDrawCanvas, 20, SpringLayout.SOUTH, spriteType); getContentPane().add(spriteDrawCanvas); // Listen for changes to sprite id in model. Update graphics accordingly model.addPropertyChangeListener(SpritePropertiesModel.PROPERTY_SPRITE_ID, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent e) { spriteDrawCanvas.setSpriteId((Integer)e.getNewValue() ); } }); spriteIdSpinner = new JSpinner(); spriteIdSpinner.setModel(new SpinnerNumberModel(0, 0, rsrc.getSpritesCount() - 1, 1)); springLayout.putConstraint(SpringLayout.WEST, spriteIdSpinner, 0, SpringLayout.WEST, lblTopleftx); springLayout.putConstraint(SpringLayout.SOUTH, spriteIdSpinner, 0, SpringLayout.SOUTH, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.EAST, spriteIdSpinner, 0, SpringLayout.EAST, lblTopleftx); getContentPane().add(spriteIdSpinner); spriteIdSpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent c) { model.setSpriteId((Integer)spriteIdSpinner.getModel().getValue() ); } }); JLabel lblId = new JLabel("ID"); springLayout.putConstraint(SpringLayout.WEST, lblId, 10, SpringLayout.WEST, this); springLayout.putConstraint(SpringLayout.SOUTH, lblId, -6, SpringLayout.NORTH, spriteIdSpinner); getContentPane().add(lblId); final JComboBox<AnimationType> animationType = new JComboBox<>(AnimationType.values() ); animationType.setSelectedItem(model.getAnimationType() ); animationType.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { final AnimationType type = (AnimationType)animationType.getSelectedItem(); model.setAnimationType(type); spriteDrawCanvas.setAnimationType(type); } }); springLayout.putConstraint(SpringLayout.WEST, animationType, 6, SpringLayout.EAST, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.SOUTH, animationType, -15, SpringLayout.SOUTH, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.EAST, animationType, 4, SpringLayout.WEST, txtWidth); getContentPane().add(animationType); final JComboBox<AnimationSpeed> animationSpeed = new JComboBox<>(AnimationSpeed.values() ); animationSpeed.setSelectedItem(model.getAnimationSpeed() ); animationSpeed.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { final AnimationSpeed speed = (AnimationSpeed)animationSpeed.getSelectedItem(); model.setAnimationSpeed(speed); spriteDrawCanvas.setAnimationSpeed(speed); } }); springLayout.putConstraint(SpringLayout.WEST, animationSpeed, 6, SpringLayout.EAST, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.SOUTH, animationSpeed, 15, SpringLayout.SOUTH, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.EAST, animationSpeed, 4, SpringLayout.WEST, txtWidth); getContentPane().add(animationSpeed); forceDirection = new JCheckBox("Force Direction"); forceDirection.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { boolean sel = forceDirection.isSelected(); direction.setEnabled(sel); if (sel) { direction.setSelectedItem("Right"); model.setForcedDirection(ForcedDirection.RIGHT_UP); } else { model.setForcedDirection(ForcedDirection.NONE); } } }); direction = new JComboBox<>(new String[] { "Right/Up", "Left/Down" } ); direction.setSelectedItem("Right"); direction.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { if ("Right/Up".equals(direction.getSelectedItem() ) ) { model.setForcedDirection(ForcedDirection.RIGHT_UP); } else { model.setForcedDirection(ForcedDirection.LEFT_DOWN); } } }); boolean forceDirectionEnabled = model.getForceDirection() != ForcedDirection.NONE; forceDirection.setSelected(forceDirectionEnabled); direction.setEnabled(forceDirectionEnabled); springLayout.putConstraint(SpringLayout.WEST, direction, 6, SpringLayout.EAST, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.SOUTH, direction, 45, SpringLayout.SOUTH, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.EAST, direction, 4, SpringLayout.WEST, txtWidth); getContentPane().add(direction); springLayout.putConstraint(SpringLayout.WEST, forceDirection, 6, SpringLayout.EAST, direction); springLayout.putConstraint(SpringLayout.SOUTH, forceDirection, 0, SpringLayout.SOUTH, direction); getContentPane().add(forceDirection); twoWayFacing = new JComboBox<>(new String[] { "Two Sets Horizontal", "Two Sets Vertical" } ); twoWayFacing.setEnabled(model.isTwoWayCapable() ); twoWayFacing.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String selection = (String)twoWayFacing.getSelectedItem(); if ("Two Sets Horizontal".equals(selection) ) { model.setTwoWayFacing(TwoWayFacing.HORIZONTAL); } else { model.setTwoWayFacing(TwoWayFacing.VERTICAL); } } }); updateTwoWayFacing(); // Changing the sprite id affects the enabled-ness and selection model.addPropertyChangeListener(SpritePropertiesModel.PROPERTY_SPRITE_ID, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent e) { if (model.isTwoWayCapable() ) { twoWayFacing.setEnabled(true); updateTwoWayFacing(); } else { twoWayFacing.setEnabled(false); } } }); springLayout.putConstraint(SpringLayout.WEST, twoWayFacing, 6, SpringLayout.EAST, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.SOUTH, twoWayFacing, 75, SpringLayout.SOUTH, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.EAST, twoWayFacing, 4, SpringLayout.WEST, txtWidth); getContentPane().add(twoWayFacing); JButton btnNewButton = new JButton("Okay"); springLayout.putConstraint(SpringLayout.SOUTH, btnNewButton, 0, SpringLayout.SOUTH, spriteDrawCanvas); springLayout.putConstraint(SpringLayout.EAST, btnNewButton, 0, SpringLayout.EAST, txtWidth); btnNewButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { okay = true; setVisible(false); } }); getContentPane().add(btnNewButton); // Sync the model with the view to create initial population of values. spriteIdSpinner.setValue(model.getSpriteId() ); spriteDrawCanvas.setSpriteId(model.getSpriteId() ); final ImmutableRectangle initialBoundingBox = model.getSpriteBoundingBox(); txtTopLeftX.setText(String.valueOf(initialBoundingBox.getLocation().x() ) ); txtTopLeftY.setText(String.valueOf(initialBoundingBox.getLocation().y() ) ); txtWidth.setText(String.valueOf(initialBoundingBox.getSize().x() ) ); txtHeight.setText(String.valueOf(initialBoundingBox.getSize().y() ) ); txtStartX.setText(String.valueOf(model.getSpriteStartingLocation().x() ) ); txtStartY.setText(String.valueOf(model.getSpriteStartingLocation().y() ) ); txtVelocityX.setText(String.valueOf(model.getSpriteVelocity().x() ) ); txtVelocityY.setText(String.valueOf(model.getSpriteVelocity().y() ) ); animationType.setSelectedItem(model.getAnimationType() ); animationSpeed.setSelectedItem(model.getAnimationSpeed() ); } private void updateTwoWayFacing() { if (model.getTwoWayFacing() == TwoWayFacing.VERTICAL) { twoWayFacing.setSelectedItem("Two Sets Vertical"); } else { twoWayFacing.setSelectedItem("Two Sets Horizontal"); } } /** * * Blocking method: Launches the dialog and waits for the user to close it. When they do, returns the model object * backing the view before it closed. * * @param parent * * @param rsrc * for displaying the sprite graphics in the selector as well as knowing how many sprites are available and setting * the bounding constraints for the JSpinner * * @param id * the id of the sprite that the model should initialise to showing * * @param startingPoint * this is typically where the mouse click was on the editor. The starting X/Y fields will be auto-filled in. * * @return * the model object after the user has finished with the view. * */ public static SpritePropertiesModel launch(JComponent parent, WorldResource rsrc, int id, ImmutablePoint2D startingPoint) { final SpritePropertiesModel model = SpritePropertiesModel.newModelWithDefaults(rsrc); // Give some intelligent auto-complete based on selection model.setSpriteLocationX(startingPoint.x() ); model.setSpriteLocationY(startingPoint.y() ); model.setSpriteBoundingBoxTopLeftX(startingPoint.x() ); model.setSpriteBoundingBoxTopLeftY(startingPoint.y() ); model.setSpriteId(id); SpritePropertiesDialog dialog = new SpritePropertiesDialog(rsrc, model); dialog.setDefaultCloseOperation(DISPOSE_ON_CLOSE); dialog.setModal(true); dialog.setSize(450, 340); dialog.setLocationRelativeTo(null); dialog.setVisible(true); // Dialog is over at this point. Set model based on how it was exited to tell client. model.setOkay(dialog.okay); return model; } /** * * Blocking method: Launches the dialog and auto fills in the model and view with the values of the selected sprite, * * @param parent * * @param rsrc * for displaying the sprite graphics in the selector as well as knowing how many sprites are available and setting * the bounding constraints for the JSpinner * * @param sprite * the sprite whose properties to use to fill dialog * * @return */ public static SpritePropertiesModel launch(JComponent parent, WorldResource rsrc, Sprite sprite) { SpritePropertiesModel model = SpritePropertiesModel.fromSprite(sprite); SpritePropertiesDialog dialog = new SpritePropertiesDialog(rsrc, model); dialog.setDefaultCloseOperation(DISPOSE_ON_CLOSE); dialog.setModal(true); dialog.setSize(450, 340); dialog.setLocationRelativeTo(null); // Show dialog.setVisible(true); // Return dialog.model.setOkay(dialog.okay); return dialog.model; } }